﻿using System;
using System.Runtime.CompilerServices;
using System.Threading;

namespace O1.Kernel.IO.KeyGeneration
{
    public sealed class Snowflake : IDisposable
    {
        private const long TimestampMask = 0x3FFFFF;

        private const long Epoch = 1351606710465L;

        private const int TimestampShift = 22;

        private readonly long workerId;

        private readonly int workerIdBits;

        private readonly int workerIdShift;

        private readonly int sequenceMask;

        private SequenceCache64 cache;

        [System.Diagnostics.CodeAnalysis.SuppressMessage(
            "Microsoft.Reliability", 
            "CA2000:Dispose objects before losing scope",
            Justification = "Class scope. Handled via IDisposable implementation.")]
        public Snowflake(byte workerId)
        {
            this.workerId = workerId;
            this.workerIdBits = 8;
            var sequenceBits = 22 - this.workerIdBits;
            this.workerIdShift = sequenceBits;
            this.sequenceMask = (int)(-1L ^ (-1L << sequenceBits));
            this.cache = new SequenceCache64(new Generator(), this.sequenceMask);
        }
        
        [System.Diagnostics.CodeAnalysis.SuppressMessage(
            "Microsoft.Reliability",
            "CA2000:Dispose objects before losing scope",
            Justification = "Class scope. Handled via IDisposable implementation.")]
        public Snowflake(short workerId, SnowflakeWorkerIdRange range)
        {
            var workerBits = (int)range;
            if (workerBits == 0)
            {
                throw new SnowflakeConfigurationException(ExceptionText.SnowflakeWorkerIdRangeInvalid.ToFormat(range.ToString()));
            }

            var max = (1L << (int)range) - 1;
            if (workerId > max)
            {
                throw new SnowflakeConfigurationException(ExceptionText.SnowflakeWorkerIdRangeTooHigh.ToFormat(workerId, max));
            }

            this.workerId = workerId;
            this.workerIdBits = workerBits;
            var sequenceBits = 22 - this.workerIdBits;
            this.workerIdShift = sequenceBits;
            this.sequenceMask = (int)(-1L ^ (-1L << sequenceBits));
            this.cache = new SequenceCache64(new Generator(), this.sequenceMask);
        }

        public long NextId()
        {
            var timestamp = this.cache.NextId() - Epoch;
            var sequence = timestamp & this.sequenceMask;
            timestamp = timestamp & ~this.sequenceMask;

            return
                (timestamp << TimestampShift) |
                (this.workerId << this.workerIdShift) |
                sequence;
        }

        public void Dispose()
        {
            if (this.cache != null)
            {
                this.cache.Dispose();
            }

            GC.SuppressFinalize(this);
        }

        private class Generator : SequenceProvider<long>
        {
            private const long Jan1Of1970 = 621355968000000000;

            private const long MaxAheadTolerance = 128;

            private const long MinAheadTolerance = 32;

            private static long lastTimestamp;

            private long aheadRanges;

            private long aheadTolerance;

            public Generator()
            {
                this.aheadTolerance = MaxAheadTolerance;
            }

            public override long Reserve(int rangeSize)
            {
                var timestamp = (long)(DateTime.UtcNow.Ticks - Jan1Of1970) / TimeSpan.TicksPerMillisecond;
                var next = lastTimestamp;
                if (timestamp > next)
                {
                    next = timestamp + rangeSize;
                    this.aheadTolerance = MaxAheadTolerance;
                }
                else
                {
                    // This is an intentional performance bottleneck for bursts of high volume.
                    // The range size represents the counter that is used in between clock milliseconds.
                    // A tolerance is managed so the throttling effect is not at every range boundary.
                    // The tradeoff is raw performance vs. correctness if a node serving IDs fails.
                    // The number of "ahead" ranges represents the number of milliseconds the node must 
                    // take to recover, in order to not generate duplicate IDs. 
                    // Assuming recovery will always take at least 128ms.
                    if (this.aheadRanges >= this.aheadTolerance)
                    {
                        Thread.Sleep(1);
                        this.aheadTolerance = Math.Max(MinAheadTolerance, this.aheadTolerance / 2);
                        this.aheadRanges -= 1;
                        if (this.aheadRanges < MinAheadTolerance)
                        {
                            this.aheadRanges = 0;
                        }
                    }
                    else
                    {
                        this.aheadRanges++;
                    }

                    next += rangeSize;
                }

                lastTimestamp = next;
                return next;
            }
        }
    }
}